/*
 * Galaxium Messenger
 * Copyright (C) 2008 Ben Motmans <ben.motmans@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.IO;
using System.Text;
using System.Collections.Generic;

using Galaxium.Core;
using Anculus.Core;

namespace Galaxium.Protocol
{
	internal sealed class IndexedConversationLogArchive : IComparable<IndexedConversationLogArchive>
	{
		private IndexedConversationLog _log;
		
		private Stream _stream;
		private BinaryReader _reader;
		private BinaryWriter _writer;

		private string _absoluteFilename;
		private string _filename;
		private int _archiveIndex;
		
		private int _messageCount;
		private int[] _indices = new int[] { 0, 0, 0, 0, 0, 0 };
		private List<int> _indexQueue;
		
		private const int _flagMessage	= 0x01;
		private const int _flagEvent	= 0x02;
		private const int _flagSource	= 0x10;
		
		private object _sync = new object ();
		
		internal IndexedConversationLogArchive (IndexedConversationLog log, BinaryReader reader)
		{
			_log = log;
			_indexQueue = new List<int> (50);
			
			ReadArchiveMetaData (reader);
			
			_absoluteFilename = Path.Combine (log.Directory, _filename);
		}
		
		internal IndexedConversationLogArchive (IndexedConversationLog log, string absoluteFilename, string filename, int archiveIndex)
		{
			_log = log;
			_absoluteFilename = absoluteFilename;
			_filename = filename;
			_archiveIndex = archiveIndex;
			_indexQueue = new List<int> (50);
			
			_absoluteFilename = Path.Combine (log.Directory, filename);
		}
		
		internal string Filename
		{
			get { return _filename; }
		}
		
		internal string AbsoluteFilename
		{
			get { return _absoluteFilename; }
		}
		
		internal int ArchiveIndex
		{
			get { return _archiveIndex; }
			set { _archiveIndex = value; }
		}
		
		internal int MessageCount
		{
			get { return _messageCount; }
		}
		
		internal int[] Indices
		{
			get { return _indices; }
		}
		
		public int CompareTo (IndexedConversationLogArchive other)
		{
			return _archiveIndex.CompareTo (other._archiveIndex);
		}
		
		internal void Close ()
		{
			lock (_sync) {
				ReleaseStream (true);
			}
		}
		
		internal void LogMessage (ITextMessage msg)
		{
			lock (_sync) {
				RequestWriter ();
				
				bool hasSource = msg.Source != null;
				int flags = _flagMessage | (hasSource ? _flagSource : 0);

				string text = msg.GetText ();

				int len = 16; //4 (len) + 8 (time) + 4 (flags)
				
				len += GetTotalStringSize (msg.Source.UniqueIdentifier);
				len += GetTotalStringSize (msg.Source.DisplayName);
				len += GetTotalStringSize (text);

				_writer.Write (len);
				_writer.Write (msg.TimeStamp.Ticks);
				_writer.Write (flags);

				if (hasSource) {
					_writer.Write (msg.Source.UniqueIdentifier);
					_writer.Write (msg.Source.DisplayName);
				}
				_writer.Write (text);
				
				UpdateIndices (len);
				ReleaseWriter (false);
			}
		}

		internal void LogEvent (DateTime timestamp, string description)
		{
			lock (_sync) {
				RequestWriter ();
				
				int len = 16; //4 (len) + 8 (time) + 4 (flags)
				len += GetTotalStringSize (description);
				
				_writer.Write (len);
				_writer.Write (timestamp.Ticks);
				_writer.Write (_flagEvent);
				
				_writer.Write (description);
				
				UpdateIndices (len);
				ReleaseWriter (false);
			}
		}
		
		private void UpdateIndices (int diff)
		{
			_messageCount++;

			bool pop = false;
			if (_messageCount > 50) {
				_indices[0] += _indexQueue[0];
				pop = true;
			}
			
			if (_messageCount > 40)
				_indices[1] += _indexQueue[_indexQueue.Count - 40];
			
			if (_messageCount > 30)
				_indices[2] += _indexQueue[_indexQueue.Count - 30];
			
			if (_messageCount > 20)
				_indices[3] += _indexQueue[_indexQueue.Count - 20];
			
			if (_messageCount > 10)
				_indices[4] += _indexQueue[_indexQueue.Count - 10];
			
			if (_messageCount > 5)
				_indices[5] += _indexQueue[_indexQueue.Count - 5];
			
			if (pop)
				_indexQueue.RemoveAt (0);
			_indexQueue.Add (diff);

			_log.MetaData.WriteIndex ();
		}
		
		internal IEnumerable<ConversationLogEntry> GetNLastEntries (int n)
		{
			lock (_sync) {
				RequestReader ();

				int remaining = 0;
				SeekToNLastEntry (n, out remaining);
				if (remaining > n)
					SkipNEntries (remaining - n);
				
				List<ConversationLogEntry> entries = new List<ConversationLogEntry> ();
				
				ConversationLogEntry entry = ReadConversationLogEntry ();
				while (entry != null) {
					entries.Add (entry);
					
					entry = ReadConversationLogEntry ();
				}
				
				ReleaseReader (false);
				
				return entries;
			}
		}

		internal IEnumerable<ConversationLogEntry> SearchAll (string keyword)
		{
			lock (_sync) {
				RequestReader ();
				_stream.Seek (0, SeekOrigin.Begin);
			
				List<ConversationLogEntry> entries = new List<ConversationLogEntry> ();
				
				ConversationLogEntry entry = ReadConversationLogEntry ();
				while (entry != null) {
					if (entry.Message != null && entry.Message.Contains (keyword))
						entries.Add (entry);
				
					entry = ReadConversationLogEntry ();
				}
			
				ReleaseReader (false);
				
				return entries;
			}
		}
		
		internal ConversationLogEntry Search (string keyword)
		{
			lock (_sync) {
				RequestReader ();
				_stream.Seek (0, SeekOrigin.Begin);
			
				ConversationLogEntry entry = ReadConversationLogEntry ();
				while (entry != null) {
					if (entry.Message != null && entry.Message.Contains (keyword))
						return entry;
					
					entry = ReadConversationLogEntry ();
				}
			
				ReleaseReader (false);
			}
			
			return null;
		}
		
		internal ConversationLogEntry SearchNext (string keyword, ConversationLogEntry entry)
		{
			lock (_sync) {
				RequestReader ();
				_stream.Seek (entry.StreamIndex, SeekOrigin.Begin);
			
				ConversationLogEntry next = ReadConversationLogEntry ();
				while (next != null) {
					if (next.Message != null && next.Message.Contains (keyword))
						return next;
					
					next = ReadConversationLogEntry ();
				}
			
				ReleaseReader (false);
			}
			
			return null;
		}
		
		internal void ReadArchiveMetaData (BinaryReader reader)
		{
			_filename = reader.ReadString ();
			_archiveIndex = reader.ReadInt32 ();
			
			_messageCount = reader.ReadInt32 ();
			
			for (int i=0; i < _indices.Length; i++)
				_indices[i] = reader.ReadInt32 ();
			
			int len = _messageCount > 50 ? 50 : _messageCount;
			for (int i=0; i < len; i++)
				_indexQueue.Add (reader.ReadInt32 ());
		}
		
		internal void WriteArchiveMetaData (IndexedConversationLogArchive archive, BinaryWriter writer)
		{
			writer.Write (_filename);
			writer.Write (_archiveIndex);
			
			writer.Write (_messageCount);
			
			for (int i=0; i < _indices.Length; i++)
				writer.Write (_indices[i]);

			int len = _indexQueue.Count;
			for (int i=0; i < len; i++)
				writer.Write (_indexQueue[i]);
		}
		
		internal bool IsMaximumSizeReached ()
		{
			FileInfo fi = new FileInfo (_absoluteFilename);
			if (fi.Length >= _log.MetaData.MaximumArchiveSize)
				return true;
			return false;
		}
		
		private void SkipNEntries (int n)
		{
			while (n-- >= 0) {
				int len = _reader.ReadInt32 ();
				_stream.Seek (len, SeekOrigin.Current);
			}
		}
		
		private void SeekToNLastEntry (int n, out int remaining)
		{
			int index = -1;
			if (n > 50) {
				index = -1;
				remaining = _messageCount;
			} else if (n > 40) {
				index = 0;
				remaining = _messageCount > 50 ? 50 : _messageCount;
			} else if (n > 30) {
				index = 1;
				remaining = _messageCount > 40 ? 40 : _messageCount;
			} else if (n > 20) {
				index = 2;
				remaining = _messageCount > 30 ? 30 : _messageCount;
			} else if (n > 10) {
				index = 3;
				remaining = _messageCount > 20 ? 20 : _messageCount;
			} else if (n > 5) {
				index = 4;
				remaining = _messageCount > 10 ? 10 : _messageCount;
			} else {
				index = 5;
				remaining = _messageCount > 5 ? 5 : _messageCount;
			}
			
			if (index >= 0) {
				int position = _indices[index];
				_stream.Seek (position, SeekOrigin.Begin);
			} else {
				_stream.Seek (0, SeekOrigin.Begin);
			}
		}
		
		private void RequestStream ()
		{
			if (_stream == null)
				_stream = new FileStream (_absoluteFilename, FileMode.OpenOrCreate, FileAccess.ReadWrite);
		}
		
		private void ReleaseStream (bool force)
		{
			if (!_log.KeepAlive || force) {
				if (_stream != null) {
					_stream.Close ();
					_stream.Dispose ();
					_stream = null;
				}
			}
		}
		
		private void RequestReader ()
		{
			if (_writer != null) //make sure the writer doesn't lock the stream
				ReleaseWriter (true);
			
			if (_reader == null) {
				RequestStream ();

				_reader = new BinaryReader (_stream, Encoding.UTF8);
			}
		}
		
		private void ReleaseReader (bool force)
		{
			if (!_log.KeepAlive || force) {
				if (_reader != null)
					_reader.Close ();
				else if (_stream != null)
					ReleaseStream (true);
				
				_reader = null;
				_stream = null; //the stream is closed automatically
			}
		}
		
		private void RequestWriter ()
		{
			if (_reader != null) //make sure the reader doesn't lock the stream
				ReleaseReader (true);
			
			if (_writer == null) {
				RequestStream ();
				_stream.Seek (_stream.Length, SeekOrigin.Begin);
			
				_writer = new BinaryWriter (_stream, Encoding.UTF8);
			}
		}
		
		private void ReleaseWriter (bool force)
		{
			if (!_log.KeepAlive || force) {
				if (_writer != null)
					_writer.Close ();
				else if (_stream != null)
					ReleaseStream (true);
				
				_writer = null;
				_stream = null; //the stream is closed automatically
			} else {
				_writer.Flush ();
			}
		}
		
		private ConversationLogEntry ReadConversationLogEntry ()
		{
			if (_stream.Position >= _stream.Length)
				return null;
			
			int index = (int)_stream.Position;
			_reader.ReadInt32 (); //the length of the entry
			DateTime timestamp = new DateTime (_reader.ReadInt64 ());
			int flags = _reader.ReadInt32 ();

			if ((flags & _flagMessage) > 0) {
				string uid = null;
				string displayName = null;
				
				if ((flags & _flagSource) > 0) {
					uid = _reader.ReadString ();
					displayName = _reader.ReadString ();
				}
				string msg = _reader.ReadString ();
				return new ConversationLogEntry (_log, _archiveIndex, index, timestamp, uid, displayName, msg);
			} else if ((flags & _flagEvent) > 0) {
				string msg = _reader.ReadString ();
				return new ConversationLogEntry (_log, _archiveIndex, index, timestamp, msg);
			} else {
				Log.Error ("Incorrect log file format.");
				return null;
			}
		}
		
		private static int GetTotalStringSize (string str)
		{
			int len = Encoding.UTF8.GetBytes (str).Length;
			int value = len;
			int prefix = 0;
			
			do {
				int high = (value >> 7) & 0x01ffffff;
				byte b = (byte)(value & 0x7f);

				if (high != 0) {
					b = (byte)(b | 0x80);
				}

				prefix++;
				value = high;
			} while (value != 0);
			
			return len + prefix;
		}
	}
}